// Copyright 2020 The searKing Author. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package ast

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"path"

	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/types/pluginpb"
)

var (
	fset = token.NewFileSet()
)

type Generator struct {
	// Ast goFiles to which this package contains.
	goFiles []GoFile

	// proto's astFile info
	protoFiles []FileInfo

	protoGenerator *protogen.Plugin
}

func NewGenerator(protoFiles []FileInfo, protoGenerator *protogen.Plugin) *Generator {
	return &Generator{
		protoFiles:     protoFiles,
		protoGenerator: protoGenerator,
	}
}

// ParseGoContent analyzes the single package constructed from the patterns and tags.
// ParseGoContent exits if there is an error.
func (g *Generator) ParseGoContent(outerFile *pluginpb.CodeGeneratorResponse_File) {
	if outerFile == nil || outerFile.GetContent() == "" {
		return
	}
	const mode = parser.AllErrors | parser.ParseComments

	f, err := parser.ParseFile(fset, "", outerFile.GetContent(), mode)
	if err != nil {
		g.protoGenerator.Error(fmt.Errorf("failed to parse struct tag in field extension: %w", err))
		return
	}
	g.addGoFile(f, outerFile)
}

// addGoFile adds a type checked Package and its syntax goFiles to the generator.
func (g *Generator) addGoFile(astFile *ast.File, outerFile *pluginpb.CodeGeneratorResponse_File) {
	g.goFiles = append(g.goFiles, GoFile{
		goGenerator: g,
		astFile:     astFile,
		fset:        fset,
		protoFiles:  g.protoFiles,
		outerFile:   outerFile,
	})
}

// Generate produces the rewrite content to proto's Generator.
func (g *Generator) Generate(module string) {
	for _, file := range g.goFiles {
		// Set the state for this run of the walker.
		if file.astFile != nil {
			ast.Inspect(file.astFile, file.genDecl)
		}

		//if file.fileChanged {
		// FIXME: always generate *.pb.go, to replace protoc-go, avoid "Tried to write the same file twice"
		{
			PrintComment(file.HasEnum(), file.astFile)
			var buf bytes.Buffer
			err := format.Node(&buf, file.fset, file.astFile)
			if err != nil {
				g.protoGenerator.Error(fmt.Errorf("failed to format go content: %w", err))
				continue
			}

			// fix Response will always be generated, so add a new generated file directly.
			//content := buf.String()
			//file.outerFile.Content = &content

			_, err = g.protoGenerator.NewGeneratedFile(path.Join(module, file.outerFile.GetName()), "").Write(buf.Bytes())
			if err != nil {
				g.protoGenerator.Error(fmt.Errorf("failed to new generated file to rewrite: %w", err))
				continue
			}
		}
	}
}

var (
	mcubeGenerate = &ast.Comment{Text: "//go:generate  mcube enum -m -p"}
	blankLine     = &ast.Comment{Text: "//"}
	remarkComment = &ast.Comment{Text: "// Code generated by protoc-gen-go-ext. DO NOT EDIT."}
)

// PrintComment todo
func PrintComment(hasEnum bool, file *ast.File) {
	var extComments []*ast.Comment

	if file == nil {
		return
	}

	if len(file.Comments) == 0 {
		file.Comments = []*ast.CommentGroup{{}}
	}

	if hasEnum {
		extComments = append(extComments, mcubeGenerate, blankLine)
	}

	pkgComments := file.Comments[0]
	if len(pkgComments.List) == 0 {
		pkgComments.List = []*ast.Comment{}
	}

	extComments = append(extComments, remarkComment)

	pkgComments.List = append(extComments, pkgComments.List...)
}
